package com.ayu.shortlink.engine.service.impl;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ayu.shortlink.common.cache.CaffeineCacheFactory;
import com.ayu.shortlink.common.cache.CommonCacheService;
import com.ayu.shortlink.engine.common.biz.user.UserContext;
import com.ayu.shortlink.engine.common.constant.RedisCacheConstant;
import com.ayu.shortlink.engine.common.convention.exception.ClientException;
import com.ayu.shortlink.engine.common.convention.exception.ServiceException;
import com.ayu.shortlink.engine.common.enums.BucketTokenKeyEnum;
import com.ayu.shortlink.engine.common.enums.LocalMessageStatusEnum;
import com.ayu.shortlink.engine.common.enums.RedisStockDecrementErrorEnum;
import com.ayu.shortlink.engine.common.enums.UserCouponStatusEnum;
import com.ayu.shortlink.engine.dao.entity.CouponTemplateDO;
import com.ayu.shortlink.engine.dao.entity.LocalMessageDO;
import com.ayu.shortlink.engine.dao.entity.UserCouponDO;
import com.ayu.shortlink.engine.dao.mapper.CouponTemplateMapper;
import com.ayu.shortlink.engine.dao.mapper.LocalMessageMapper;
import com.ayu.shortlink.engine.dao.mapper.UserCouponMapper;
import com.ayu.shortlink.engine.dto.req.UserRedeemCouponReqDTO;
import com.ayu.shortlink.engine.mq.basics.event.DeductCouponTransactionEvent;
import com.ayu.shortlink.engine.mq.basics.event.UserCouponOnTimeCloseEvent;
import com.ayu.shortlink.engine.mq.basics.event.UserGainCouponEvent;
import com.ayu.shortlink.engine.mq.producer.DeductCouponTransactionProducer;
import com.ayu.shortlink.engine.mq.producer.UserCouponOnTimeCloseProducer;
import com.ayu.shortlink.engine.mq.producer.UserGainCouponProducer;
import com.ayu.shortlink.engine.remote.dto.IAdminService;
import com.ayu.shortlink.engine.service.UserCouponService;
import com.ayu.shortlink.engine.util.LuaScriptUtil;
import com.ayu.shortlink.engine.util.RateLimiterUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.ayu.shortlink.engine.common.constant.RocketMQConstant.TOPIC_USER_GAIN_COUPON;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCouponDO> implements UserCouponService {
    private final StringRedisTemplate stringRedisTemplate;
    private final TransactionTemplate transactionTemplate;
    private final CouponTemplateMapper couponTemplateMapper;
    private final UserCouponMapper userCouponMapper;
    private final UserCouponOnTimeCloseProducer userCouponOnTimeCloseProducer;
    private final RedissonClient redissonClient;
    private final static String STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH = "lua/stock_decrement_and_save_user_receive.lua";
    private final static String OBTAIN_COUPON_NUMBER_LUA_PATH = "lua/obtain_coupon_number.lua";
    private final IAdminService iAdminService;
    private final DeductCouponTransactionProducer deductCouponTransactionProducer;
    private final LocalMessageMapper localMessageMapper;
    private final UserGainCouponProducer userGainCouponProducer;
    private final RateLimiterUtil rateLimiterUtil;
    private final Object LOCK = new Object();
    @Override
    public void redeemCoupon(UserRedeemCouponReqDTO requestParam) {
        CouponTemplateDO couponTemplate = iAdminService.findCouponTemplate(requestParam.getShopNumber(), requestParam.getCouponTemplateId()).getData();
        if(couponTemplate == null){
            throw new ClientException("优惠券不存在");
        }
        // 验证领取的优惠券是否在活动有效时间
        boolean isInTime = DateUtil.isIn(new Date(), couponTemplate.getValidStartTime(), couponTemplate.getValidEndTime());
        if (!isInTime) {
            // 一般来说优惠券领取时间不到的时候，前端不会放开调用请求，可以理解这是用户调用接口在“攻击”
            throw new ClientException("不满足优惠券领取时间");
        }
        StringBuilder var = new StringBuilder()
                .append(requestParam.getShopNumber())
                .append("_")
                .append(requestParam.getCouponTemplateId());

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(RedisCacheConstant.LOCK_COUPON_TEMPLATE_STOCK_CHANGE, var));
        RLock rLock = readWriteLock.readLock();

        rLock.lock();
        try {
            // 用 HuTool 的单例容器管理 lua 脚本
            DefaultRedisScript<Long> script = Singleton.get(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH,() -> {
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH)));
                redisScript.setResultType(Long.class);
                return redisScript;
            });
            // 验证用户是否符合优惠券领取条件
            JSONObject receiveRule = JSON.parseObject(couponTemplate.getReceiveRule());
            String limitPerPerson = receiveRule.getString("limitPerPerson");

            // 获取库存容量的 KEY
            String couponTemplateCapacityKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);

            // 用户获取当前优惠券次数的 KEY
            String userObtainCouponKey = String.format(RedisCacheConstant.USER_OBTAIN_COUPON_TIME_KEY, UserContext.getUserId(), couponTemplateCapacityKey);

            Long result = stringRedisTemplate.execute(script,
                    List.of(couponTemplateCapacityKey, userObtainCouponKey),
                    String.valueOf(couponTemplate.getValidEndTime().getTime()), limitPerPerson);

            long extractFirstField = LuaScriptUtil.extractFirstField(result);
            if(!LuaScriptUtil.isSuccess(extractFirstField)){
                throw new ServiceException(RedisStockDecrementErrorEnum.fromType(extractFirstField));
            }

            transactionTemplate.executeWithoutResult(status -> {
                try {
                    long receiveCount = LuaScriptUtil.extractSecondField(result, extractFirstField);
                    //进行库存扣减
                    Long deducted = couponTemplateMapper.deductCouponTemplateStock(1L, Long.valueOf(requestParam.getShopNumber()), Long.valueOf(requestParam.getCouponTemplateId()));

                    if(!SqlHelper.retBool(deducted)){
                        throw new ServiceException("很抱歉，已经无库存");
                    }

                    UserCouponDO userCoupon = UserCouponDO.builder()
                            .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                            .userId(Long.valueOf(UserContext.getUserId()))
                            .source(requestParam.getSource())
                            .status(UserCouponStatusEnum.UNUSED.getCode())
                            .validStartTime(couponTemplate.getValidStartTime())
                            .validEndTime(couponTemplate.getValidEndTime())
                            .receiveCount((int) receiveCount)
                            .build();
                    userCouponMapper.insert(userCoupon);

                    // 需要发MQ定时改变优惠券状态为不可用
                    UserCouponOnTimeCloseEvent event = UserCouponOnTimeCloseEvent.builder()
                            .userId(List.of(Long.valueOf(UserContext.getUserId())))
                            .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                            .build();
                    Long deliverTime = DateUtil.between(new Date(), couponTemplate.getValidEndTime(), DateUnit.MS);
                    userCouponOnTimeCloseProducer.sendMessage(event,deliverTime);
                }catch (Throwable throwable){
                    //设置事务回滚
                    status.setRollbackOnly();
                    // 自减用户领取的优惠券记录 xxx_优惠券ID_用户ID Value 是领取次数
                    if (throwable instanceof ServiceException) {
                        // 进行用户优惠券记录减少
                        stringRedisTemplate.opsForValue().decrement(userObtainCouponKey, 1);
                        throw (ServiceException) throwable;
                    }else if (throwable instanceof DuplicateKeyException) {
                        log.error("用户重复领取优惠券，用户ID：{}，优惠券模板ID：{}", UserContext.getUserId(), requestParam.getCouponTemplateId());
                        // 进行用户优惠券记录减少
                        stringRedisTemplate.opsForValue().decrement(userObtainCouponKey, 1);
                        throw new ServiceException("用户重复领取优惠券");
                    }
                    throw new ServiceException("优惠券领取异常，请稍候再试");
                }
            });
        }finally {
            rLock.unlock();
        }

    }

    /**
     * 优化的点：使用RocketMQ事务消息 将一些其余逻辑都丢到下游消费者上 这个专注于库存扣减即可
     * @param requestParam 请求参数
     */
    @Override
    public void redeemCoupon2(UserRedeemCouponReqDTO requestParam) {
        CouponTemplateDO couponTemplate = iAdminService.findCouponTemplate(requestParam.getShopNumber(), requestParam.getCouponTemplateId()).getData();
        if(couponTemplate == null){
            throw new ClientException("优惠券不存在");
        }
        // 验证领取的优惠券是否在活动有效时间
        boolean isInTime = DateUtil.isIn(new Date(), couponTemplate.getValidStartTime(), couponTemplate.getValidEndTime());
        if (!isInTime) {
            // 一般来说优惠券领取时间不到的时候，前端不会放开调用请求，可以理解这是用户调用接口在“攻击”
            throw new ClientException("不满足优惠券领取时间");
        }
        StringBuilder var = new StringBuilder()
                .append(requestParam.getShopNumber())
                .append("_")
                .append(requestParam.getCouponTemplateId());

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(RedisCacheConstant.LOCK_COUPON_TEMPLATE_STOCK_CHANGE, var));
        RLock rLock = readWriteLock.readLock();

        rLock.lock();
        try {
            // 用 HuTool 的单例容器管理 lua 脚本
            DefaultRedisScript<Long> script = Singleton.get(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH,() -> {
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH)));
                redisScript.setResultType(Long.class);
                return redisScript;
            });
            // 验证用户是否符合优惠券领取条件
            JSONObject receiveRule = JSON.parseObject(couponTemplate.getReceiveRule());
            String limitPerPerson = receiveRule.getString("limitPerPerson");

            // 获取库存容量的 KEY
            String couponTemplateCapacityKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);

            // 用户获取优惠券次数的 KEY
            String userObtainCouponKey = String.format(RedisCacheConstant.USER_OBTAIN_COUPON_TIME_KEY, UserContext.getUserId(), couponTemplateCapacityKey);

            Long result = stringRedisTemplate.execute(script,
                    List.of(couponTemplateCapacityKey, userObtainCouponKey),
                    String.valueOf(couponTemplate.getValidEndTime().getTime()), limitPerPerson);

            long extractFirstField = LuaScriptUtil.extractFirstField(result);
            if(!LuaScriptUtil.isSuccess(extractFirstField)){
                throw new ServiceException(RedisStockDecrementErrorEnum.fromType(extractFirstField));
            }

            DeductCouponTransactionEvent event = DeductCouponTransactionEvent.builder()
                    .shopNumber(UserContext.getShopNumber())
                    .stock(1L)
                    .couponTemplateId(couponTemplate.getId())
                    .build();
            TransactionSendResult sendResult = deductCouponTransactionProducer.sendMessageInTransaction(event);

            if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)){
                log.error("发送失败:{}", event);
                //TODO 记录在数据库 然后后期可以用定时任务扫描执行
                throw new ServiceException("系统出现异常");
            }
        }finally {
            rLock.unlock();
        }
    }

    /**
     * 优化的点：
     * 1、前面对Redis的依赖性过高，如果Redis宕机了(不考虑Redis主从)，那么整个服务就不可用了，所以这个方案对Redis的依赖性降低了
     * 2、解决了Redis的hotKey会存在的性能瓶颈
     * @param requestParam 请求参数
     */
    @Override
    public void redeemCoupon3(UserRedeemCouponReqDTO requestParam) {
        CouponTemplateDO couponTemplate = iAdminService.findCouponTemplate(requestParam.getShopNumber(), requestParam.getCouponTemplateId()).getData();
        if(couponTemplate == null){
            throw new ClientException("优惠券不存在");
        }
        // 验证领取的优惠券是否在活动有效时间
        boolean isInTime = DateUtil.isIn(new Date(), couponTemplate.getValidStartTime(), couponTemplate.getValidEndTime());
        if (!isInTime) {
            // 一般来说优惠券领取时间不到的时候，前端不会放开调用请求，可以理解这是用户调用接口在“攻击”
            throw new ClientException("不满足优惠券领取时间");
        }
        StringBuilder var = new StringBuilder()
                .append(requestParam.getShopNumber())
                .append("_")
                .append(requestParam.getCouponTemplateId());
        // 获取库存容量的 KEY
        String couponTemplateCapacityKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);

        // 本地缓存获取票数
        CommonCacheService<String, Integer> couponNumberCache = CaffeineCacheFactory.getCouponNumberCache();
        Integer couponNumber = couponNumberCache.get(var.toString());
        // 如果本地缓存没有票数 那么首先去请求Redis获取一部分票数(比如每次请求1000)
        // 如果Redis宕机 那么去MySQL获取票数
        if (couponNumber == null || couponNumber <= 0){
            // 获取时可能会出现线程安全问题 使用双重判定 来避免线程安全问题
            synchronized (LOCK){
                couponNumber = couponNumberCache.get(var.toString());
                // 双重判定
                if (couponNumber == null || couponNumber <= 0){
                    boolean obtained = obtainCouponNumber(couponTemplateCapacityKey,var.toString(),requestParam);
                    // 获取失败 那么证明Redis和MySQL可能都有问题
                    if (!obtained){
                        throw new ServiceException("抢优惠券服务异常");
                    }
                }
            }
        }
        // 进行原子扣减操作
        Integer compute = couponNumberCache.getMap().compute(var.toString(), (k, v) -> {
            if (v == null || v <= 0) {
                return -1;
            }
            return v - 1;
        });
        // 扣减失败 但是可能会存在
        if (compute < 0){
            throw new ServiceException("优惠券已抢光");
        }

        // 扣减成功之后需要进行数据库库存的扣减
        // TODO 先直接扣减 方便压测
        transactionTemplate.executeWithoutResult(status -> {
            try {
                //进行库存扣减
                Long deducted = couponTemplateMapper.deductCouponTemplateStock(1L, Long.valueOf(requestParam.getShopNumber()), Long.valueOf(requestParam.getCouponTemplateId()));

                if(!SqlHelper.retBool(deducted)){
                    throw new ServiceException("很抱歉，已经无库存");
                }

                UserCouponDO userCoupon = UserCouponDO.builder()
                        .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                        .userId(Long.valueOf(UserContext.getUserId()))
                        .source(requestParam.getSource())
                        .status(UserCouponStatusEnum.UNUSED.getCode())
                        .validStartTime(couponTemplate.getValidStartTime())
                        .validEndTime(couponTemplate.getValidEndTime())
                        .consumeRule(couponTemplate.getConsumeRule())
                        .type(couponTemplate.getType())
                        .build();
                userCouponMapper.insert(userCoupon);

            }catch (Throwable throwable){
                //设置事务回滚
                status.setRollbackOnly();
                throw new ServiceException("优惠券领取异常，请稍候再试");
            }
        });
    }

    @Override
    public void redeemCoupon4(UserRedeemCouponReqDTO requestParam) {
        // 通过本地缓存来扛 一些DB已经确切没有的优惠券
        CommonCacheService<String, String> couponNotStock = CaffeineCacheFactory.getCouponNotStock();
        if (StrUtil.isNotBlank(
                couponNotStock.get(requestParam.getShopNumber()+"_"+requestParam.getCouponTemplateId()))){
            throw new ServiceException("很抱歉，已经无库存");
        }

        CouponTemplateDO couponTemplate = iAdminService.findCouponTemplate(requestParam.getShopNumber(), requestParam.getCouponTemplateId()).getData();
        if(couponTemplate == null){
            throw new ClientException("优惠券不存在");
        }
        // 验证领取的优惠券是否在活动有效时间
        boolean isInTime = DateUtil.isIn(new Date(), couponTemplate.getValidStartTime(), couponTemplate.getValidEndTime());
        if (!isInTime) {
            // 一般来说优惠券领取时间不到的时候，前端不会放开调用请求，可以理解这是用户调用接口在“攻击”
            throw new ClientException("不满足优惠券领取时间");
        }
        StringBuilder var = new StringBuilder()
                .append(requestParam.getShopNumber())
                .append("_")
                .append(requestParam.getCouponTemplateId());
        // 获取库存容量的 KEY
        String couponTemplateCapacityKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);

        Long decrement = null;
        try {
            decrement = stringRedisTemplate.opsForValue().decrement(couponTemplateCapacityKey);
        }catch (RedisConnectionFailureException exception){
            log.error("Redis服务器异常，请速速检查");
            // TODO Redis宕机 需要报警 并执行降级方案
            boolean acquire = rateLimiterUtil.tryAcquire(BucketTokenKeyEnum.OBTAIN_COUPON_TOKEN_KEY.getKey(),
                    2, 5, TimeUnit.SECONDS);

            if (!acquire){
                throw new ClientException("当前人数过多，请稍后再试");
            }
            // 若获取成功则为true(默认也为true) 若获取失败则为false
            AtomicBoolean gainSuccess = new AtomicBoolean(true);
            transactionTemplate.executeWithoutResult(status -> {
                try {
                    //进行库存扣减
                    Long deducted = couponTemplateMapper.deductCouponTemplateStock(1L, Long.valueOf(requestParam.getShopNumber()), Long.valueOf(requestParam.getCouponTemplateId()));

                    if(SqlHelper.retBool(deducted)){
                        UserCouponDO userCoupon = UserCouponDO.builder()
                                .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                                .userId(Long.valueOf(UserContext.getUserId()))
                                .source(requestParam.getSource())
                                .status(UserCouponStatusEnum.UNUSED.getCode())
                                .validStartTime(couponTemplate.getValidStartTime())
                                .validEndTime(couponTemplate.getValidEndTime())
                                .consumeRule(couponTemplate.getConsumeRule())
                                .type(couponTemplate.getType())
                                .build();
                        userCouponMapper.insert(userCoupon);
                    }else {
                        // 设置库存为空
                        couponNotStock.put(requestParam.getShopNumber()+"_"+requestParam.getCouponTemplateId(),"-");
                        gainSuccess.set(false);
                    }
                }catch (Throwable throwable){
                    //设置事务回滚
                    status.setRollbackOnly();
                    throw new ServiceException("优惠券领取异常，请稍候再试");
                }

                if (!gainSuccess.get()){
                    throw new ServiceException("很抱歉，已经无库存");
                }
            });
            return;
        }

        // 表明说Redis中已经没有票了
        if (decrement == null || decrement < 0){
            throw new ServiceException("很抱歉，已经无库存");
        }

        // 若获取成功则为true(默认也为true) 若获取失败则为false
        AtomicBoolean gainSuccess = new AtomicBoolean(true);

        // 扣减成功之后需要进行数据库库存的扣减
        // TODO 先直接扣减 方便压测
        transactionTemplate.executeWithoutResult(status -> {
            try {
                //进行库存扣减
                Long deducted = couponTemplateMapper.deductCouponTemplateStock(1L, Long.valueOf(requestParam.getShopNumber()), Long.valueOf(requestParam.getCouponTemplateId()));

                if(SqlHelper.retBool(deducted)){
                    UserCouponDO userCoupon = UserCouponDO.builder()
                            .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                            .userId(Long.valueOf(UserContext.getUserId()))
                            .source(requestParam.getSource())
                            .status(UserCouponStatusEnum.UNUSED.getCode())
                            .validStartTime(couponTemplate.getValidStartTime())
                            .validEndTime(couponTemplate.getValidEndTime())
                            .consumeRule(couponTemplate.getConsumeRule())
                            .type(couponTemplate.getType())
                            .build();
                    userCouponMapper.insert(userCoupon);
                }else {
                    // 设置库存为空
                    couponNotStock.put(requestParam.getShopNumber()+"_"+requestParam.getCouponTemplateId(),"-");
                    gainSuccess.set(false);
                }
            }catch (Throwable throwable){
                //设置事务回滚
                status.setRollbackOnly();
                throw new ServiceException("优惠券领取异常，请稍候再试");
            }
        });
        if (!gainSuccess.get()){
            throw new ServiceException("很抱歉，已经无库存");
        }
    }

    @Override
    public void redeemCoupon5(UserRedeemCouponReqDTO requestParam) {
        // 通过本地缓存来扛 一些DB已经确切没有的优惠券
        CommonCacheService<String, String> couponNotStock = CaffeineCacheFactory.getCouponNotStock();
        if (StrUtil.isNotBlank(
                couponNotStock.get(requestParam.getShopNumber()+"_"+requestParam.getCouponTemplateId()))){
            throw new ServiceException("很抱歉，已经无库存");
        }

        CouponTemplateDO couponTemplate = iAdminService.findCouponTemplate(requestParam.getShopNumber(), requestParam.getCouponTemplateId()).getData();
        if(couponTemplate == null){
            throw new ClientException("优惠券不存在");
        }
        // 验证领取的优惠券是否在活动有效时间
        boolean isInTime = DateUtil.isIn(new Date(), couponTemplate.getValidStartTime(), couponTemplate.getValidEndTime());
        if (!isInTime) {
            // 一般来说优惠券领取时间不到的时候，前端不会放开调用请求，可以理解这是用户调用接口在“攻击”
            throw new ClientException("不满足优惠券领取时间");
        }
        StringBuilder var = new StringBuilder()
                .append(requestParam.getShopNumber())
                .append("_")
                .append(requestParam.getCouponTemplateId());
        // 获取库存容量的 KEY
        String couponTemplateCapacityKey = String.format(RedisCacheConstant.COUPON_TEMPLATE_CAPACITY_KEY, var);

        Long decrement = null;
        try {
            decrement = stringRedisTemplate.opsForValue().decrement(couponTemplateCapacityKey);
        }catch (RedisConnectionFailureException exception){
            log.error("Redis服务器异常");
            // TODO Redis宕机 需要报警 并执行降级方案
            boolean acquire = rateLimiterUtil.tryAcquire(BucketTokenKeyEnum.OBTAIN_COUPON_TOKEN_KEY.getKey(),
                    2, 5, TimeUnit.SECONDS);

            if (!acquire){
                throw new ClientException("当前人数过多，请稍后再试");
            }
            // 若获取成功则为true(默认也为true) 若获取失败则为false
            AtomicBoolean gainSuccess = new AtomicBoolean(true);

            transactionTemplate.executeWithoutResult(status -> {
                try {
                    //进行库存扣减
                    Long deducted = couponTemplateMapper.deductCouponTemplateStock(1L, Long.valueOf(requestParam.getShopNumber()), Long.valueOf(requestParam.getCouponTemplateId()));

                    if(SqlHelper.retBool(deducted)){
                        UserCouponDO userCoupon = UserCouponDO.builder()
                                .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                                .userId(Long.valueOf(UserContext.getUserId()))
                                .source(requestParam.getSource())
                                .status(UserCouponStatusEnum.UNUSED.getCode())
                                .validStartTime(couponTemplate.getValidStartTime())
                                .validEndTime(couponTemplate.getValidEndTime())
                                .consumeRule(couponTemplate.getConsumeRule())
                                .type(couponTemplate.getType())
                                .build();
                        userCouponMapper.insert(userCoupon);
                    }else {
                        // 设置库存为空
                        couponNotStock.put(requestParam.getShopNumber()+"_"+requestParam.getCouponTemplateId(),"-");
                        gainSuccess.set(false);
                    }
                }catch (Throwable throwable){
                    //设置事务回滚
                    status.setRollbackOnly();
                    throw new ServiceException("优惠券领取异常，请稍候再试");
                }
                if (!gainSuccess.get()){
                    throw new ServiceException("很抱歉，已经无库存");
                }
            });
            return;
        }

        // 表明说Redis中已经没有票了
        if (decrement == null || decrement < 0){
            throw new ServiceException("很抱歉，已经无库存");
        }

        // 若获取成功则为true(默认也为true) 若获取失败则为false
        AtomicBoolean gainSuccess = new AtomicBoolean(true);

        String messageId = UUID.randomUUID().toString();
        UserGainCouponEvent event = UserGainCouponEvent.builder()
                .couponTemplateId(Long.valueOf(requestParam.getCouponTemplateId()))
                .userId(Long.valueOf(UserContext.getUserId()))
                .source(requestParam.getSource())
                .status(UserCouponStatusEnum.UNUSED.getCode())
                .validStartTime(couponTemplate.getValidStartTime())
                .validEndTime(couponTemplate.getValidEndTime())
                .keys(messageId)
                .build();
        // 扣减成功之后需要进行数据库库存的扣减
        // TODO 先直接扣减 方便压测
        transactionTemplate.executeWithoutResult(status -> {
            try {
                //进行库存扣减
                Long deducted = couponTemplateMapper.deductCouponTemplateStock(1L, Long.valueOf(requestParam.getShopNumber()), Long.valueOf(requestParam.getCouponTemplateId()));

                if(SqlHelper.retBool(deducted)){
                    // 本地消息持久化到DB
                    LocalMessageDO localMessageDO = LocalMessageDO.builder()
                            .status(LocalMessageStatusEnum.MESSAGE_PENDING.getStatus())
                            .messageId(messageId)
                            // 消息内容 便于后续的补偿操作
                            .messageBody(JSON.toJSONString(event))
                            // 分发主题
                            .topic(TOPIC_USER_GAIN_COUPON)
                            .build();
                    localMessageMapper.insert(localMessageDO);
                }else {
                    // 设置库存为空
                    couponNotStock.put(requestParam.getShopNumber()+"_"+requestParam.getCouponTemplateId(),"-");
                    gainSuccess.set(false);
                }
            }catch (Throwable throwable){
                //设置事务回滚
                status.setRollbackOnly();
                throw new ServiceException("优惠券领取异常，请稍候再试");
            }
        });
        if (!gainSuccess.get()){
            throw new ServiceException("很抱歉，已经无库存");
        }

        // TODO 发送给admin模块
        try {
            SendResult sendResult = userGainCouponProducer.sendMessage(event);
            // 如果发送成功 则修改本地事务表的状态
            if (sendResult.getSendStatus().equals(SendStatus.SEND_OK)){
                LocalMessageDO localMessageDO = LocalMessageDO.builder()
                        .status(LocalMessageStatusEnum.MESSAGE_SEND_OK.getStatus())
                        .build();
                LambdaQueryWrapper<LocalMessageDO> queryWrapper = new LambdaQueryWrapper<>(LocalMessageDO.class)
                        .eq(LocalMessageDO::getMessageId, messageId);
                localMessageMapper.update(localMessageDO,queryWrapper);
            }
        }catch (Exception exception){
            log.error("发送消息失败");
        }
    }

    private boolean obtainCouponNumber(String couponTemplateCapacityKey,String key,UserRedeemCouponReqDTO requestParam){
        boolean redisDown = false;
        CommonCacheService<String, Integer> couponNumberCache = CaffeineCacheFactory.getCouponNumberCache();
        // 先从Redis获取
        try {
            DefaultRedisScript<Integer> script = Singleton.get(OBTAIN_COUPON_NUMBER_LUA_PATH,() -> {
                DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
                redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(OBTAIN_COUPON_NUMBER_LUA_PATH)));
                redisScript.setResultType(Integer.class);
                return redisScript;
            });
            Integer stock = stringRedisTemplate.execute(script, List.of(couponTemplateCapacityKey));
            couponNumberCache.put(key, stock);
        }catch (Exception exception){
            // 日志打印
            log.error(exception.toString());
            redisDown = true;
        }
        // 如果从Redis获取失败，那么就从MySQL获取票数，并不做实际的扣减
        if (redisDown){
            try {
                LambdaQueryWrapper<CouponTemplateDO> wrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                        .eq(CouponTemplateDO::getShopNumber, requestParam.getShopNumber())
                        .eq(CouponTemplateDO::getId, requestParam.getCouponTemplateId())
                        .select(CouponTemplateDO::getStock);
                CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(wrapper);

                Integer stock = couponTemplateDO.getStock();
                stock = Math.min(stock, 200);
                couponNumberCache.put(key, stock);
            }catch (Exception exception){
                log.error(exception.toString());
                return false;
            }
        }
        return true;
    }
}
